#include <limits.h>
#include "psyspp.h"
#include "sdrman.h"
#include "logger.h"
#include "mesh.h"	// just for the attrib enums
#include "unistate.h"
#include "datapath.h"
#include "texman.h"
#include "texgen.h"

using namespace goatgfx;

static void pdraw_start(const psys_emitter *em, void *cls);
static void pdraw(const psys_emitter *em, const psys_particle *part, void *cls);
static void pdraw_end(const psys_emitter *em, void *cls);

static unsigned int psys_load_texture(const char *fname, void *cls);

ParticleSystemAttributes::ParticleSystemAttributes()
{
	tex = 0;
	own_psattr = true;
	psattr = new psys_attributes;
	psys_init_attr(psattr);
}

ParticleSystemAttributes::ParticleSystemAttributes(psys_attributes *psattr)
{
	tex = 0;
	own_psattr = false;
	this->psattr = psattr;
}

ParticleSystemAttributes::~ParticleSystemAttributes()
{
	if(own_psattr) {
		psys_destroy_attr(psattr);
		delete psattr;
	}
}

ParticleSystemAttributes::ParticleSystemAttributes(const ParticleSystemAttributes &rhs)
{
	own_psattr = true;
	tex = rhs.tex;
	psattr = new psys_attributes;
	psys_init_attr(psattr);

	psys_copy_attr(psattr, rhs.psattr);
}

ParticleSystemAttributes &ParticleSystemAttributes::operator =(const ParticleSystemAttributes &rhs)
{
	if(&rhs != this) {
		tex = rhs.tex;
		psys_copy_attr(psattr, rhs.psattr);
	}
	return *this;
}

bool ParticleSystemAttributes::load(const char *fname)
{
	psys_texture_loader(psys_load_texture, 0, this);
	return psys_load_attr(psattr, goatgfx::datafile_path(fname).c_str()) != -1;
}

bool ParticleSystemAttributes::load(FILE *fp)
{
	psys_texture_loader(psys_load_texture, 0, this);
	return psys_load_attr_stream(psattr, fp) != -1;
}

bool ParticleSystemAttributes::save(const char *fname) const
{
	return psys_save_attr(psattr, fname) != -1;
}

bool ParticleSystemAttributes::save(FILE *fp) const
{
	return psys_save_attr_stream(psattr, fp) != -1;
}

void ParticleSystemAttributes::set_part_color(const Vector3 &color, float t)
{
	psys_set_value3(&psattr->part_attr.color, (anm_time_t)(t * 1000.0), v3_cons(color.x, color.y, color.z));
}

void ParticleSystemAttributes::set_part_alpha(float alpha, float t)
{
	psys_set_value(&psattr->part_attr.alpha, (anm_time_t)(t * 1000.0), alpha);
}

void ParticleSystemAttributes::set_part_scale(float size, float t)
{
	psys_set_value(&psattr->part_attr.size, (anm_time_t)(t * 1000.0), size);
}


// emmiter attributes
void ParticleSystemAttributes::set_texture(Texture *tex)
{
	this->tex = tex;
	psattr->tex = tex->get_id();
}

Texture *ParticleSystemAttributes::get_texture() const
{
	return tex;
}

void ParticleSystemAttributes::set_spawn_range(const Vector3 &range, long tm)
{
	psys_set_value3(&psattr->spawn_range, ANM_MSEC2TM(tm), v3_cons(range.x, range.y, range.z));
}

void ParticleSystemAttributes::set_spawn_rate(float rate, long tm)
{
	psys_set_value(&psattr->rate, ANM_MSEC2TM(tm), rate);
}

float ParticleSystemAttributes::get_spawn_rate(long tm) const
{
	return psys_get_value(&psattr->rate, ANM_MSEC2TM(tm));
}

void ParticleSystemAttributes::set_life(float life, float range, long tm)
{
	psys_set_anm_rnd(&psattr->life, ANM_MSEC2TM(tm), life, range);
}

void ParticleSystemAttributes::set_size(float sz, float range, long tm)
{
	psys_set_anm_rnd(&psattr->size, ANM_MSEC2TM(tm), sz, range);
}

void ParticleSystemAttributes::set_spawn_dir(const Vector3 &dir, const Vector3 &range, long tm)
{
	psys_set_anm_rnd3(&psattr->dir, ANM_MSEC2TM(tm), v3_cons(dir.x, dir.y, dir.z), v3_cons(range.x, range.y, range.z));
}

void ParticleSystemAttributes::set_gravity(const Vector3 &grav, long tm)
{
	psys_set_value3(&psattr->grav, ANM_MSEC2TM(tm), v3_cons(grav.x, grav.y, grav.z));
}

void ParticleSystemAttributes::set_drag(float drag)
{
	psattr->drag = drag;
}

void ParticleSystemAttributes::set_particle_limit(int lim)
{
	psattr->max_particles = lim;
}


// ---- ParticleSystem ----

ParticleSystem::ParticleSystem()
	: attr(&psys.attr)
{
	psys_init(&psys);
	psys_draw_func(&psys, pdraw, pdraw_start, pdraw_end, (void*)this);
	start_time = LONG_MIN;
	last_upd_time = LONG_MIN;
}

ParticleSystem::~ParticleSystem()
{
	psys_destroy(&psys);
}

void ParticleSystem::set_start_time(long tm)
{
	start_time = tm;
}

bool ParticleSystem::is_active() const
{
	float rate = attr.get_spawn_rate(last_upd_time);
	return psys.pcount > 0 || last_upd_time == 0;// || rate > 0.0;
}

ParticleSystemAttributes *ParticleSystem::get_attr()
{
	return &attr;
}

const ParticleSystemAttributes *ParticleSystem::get_attr() const
{
	return &attr;
}

void ParticleSystem::set_attr(const ParticleSystemAttributes &pattr)
{
	attr = pattr;
}

bool ParticleSystem::load(const char *fname)
{
	psys_texture_loader(psys_load_texture, 0, &attr);
	return attr.load(fname);
}

bool ParticleSystem::save(const char *fname) const
{
	return attr.save(fname);
}

void ParticleSystem::update(long tm)
{
	if(start_time > LONG_MIN) {
		tm -= start_time;
	}

	Matrix4x4 xform;
	get_xform(tm, &xform);

	Vector3 pos = Vector3(0, 0, 0).transformed(xform);

	psys_set_pos(&psys, v3_cons(pos.x, pos.y, pos.z), 0);
	psys_update(&psys, (double)tm / 1000.0);

	last_upd_time = tm;
}

void ParticleSystem::draw() const
{
	psys_draw(&psys);
}

// ---- particle drawing ----
struct PVertex {
	Vector4 color;
	Vector3 pos;
	Vector2 texcoord;
};


#define USE_VBO
#define USE_IBO


#define MAX_DRAW_PART	256
#define MAX_PVERTS		(MAX_DRAW_PART * 4)
static PVertex *pvarr, *pvptr;

// double-buffered vbo set, write on one, while drawing from the other

#ifdef USE_VBO
static unsigned int vbo[2];
static int cur_buf;		// current write vbo
#endif
static int num_buffered;	// number of particles bufferd, will flush when >= MAX_DRAW_PART

// ok so the index array is constant, regardless of the particle system
// so this is a static index buffer created in init_particle_draw which should
// be called once.
#define MAX_PVIDX		(MAX_DRAW_PART * 6)
#ifdef USE_IBO
static unsigned int ibo;
#endif
unsigned int *pvidx;

static ShaderProg *psdr;	// particle shader
static Texture *blank_tex;

static inline void init_particle_draw()
{
	static bool done_init;
	if(done_init) {
		return;		// once
	}

	pvidx = new unsigned int[MAX_PVIDX];
	unsigned int *ptr = pvidx;

	static const unsigned int idxoffs[] = { 0, 1, 2, 0, 2, 3 };

	for(int i=0; i<MAX_DRAW_PART; i++) {
		for(int j=0; j<6; j++) {
			*ptr++ = i * 4 + idxoffs[j];
		}
	}

#ifdef USE_IBO
	glGenBuffers(1, &ibo);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, MAX_PVIDX * sizeof *pvidx, pvidx, GL_STATIC_DRAW);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	delete [] pvidx;
#endif

#ifdef USE_VBO
	// create the stream vertex buffers (double buffering)
	glGenBuffers(2, vbo);
	for(int i=0; i<2; i++) {
		glBindBuffer(GL_ARRAY_BUFFER, vbo[i]);
		glBufferData(GL_ARRAY_BUFFER, MAX_PVERTS * sizeof(PVertex), 0, GL_STREAM_DRAW);
	}
	glBindBuffer(GL_ARRAY_BUFFER, 0);
#else
	pvarr = new PVertex[MAX_PVERTS];
	pvptr = pvarr;
#endif

	// load shader program
	if(!(psdr = get_sdrprog("psdr.v.glsl", "psdr.p.glsl"))) {
		error_log("failed to load particle shader!\n");
	}

	// create empty texture
	Image *img = texgen_solid(8, 8, Vector4(1, 1, 1, 1));
	blank_tex = new Texture;
	blank_tex->set_image(*img);
	delete img;

	done_init = true;
}

static void pdraw_flush()
{
#ifdef USE_VBO
	// assuming vbo[cur_buf] is bound
	glUnmapBuffer(GL_ARRAY_BUFFER);
#endif

	// draw from the bound buffer 6 indices per particle
#ifdef USE_IBO
	glDrawElements(GL_TRIANGLES, num_buffered * 6, GL_UNSIGNED_INT, 0);
#else
	glDrawElements(GL_TRIANGLES, num_buffered * 6, GL_UNSIGNED_INT, pvidx);
#endif
	num_buffered = 0;

#ifdef USE_VBO
	// map the next buffer (write buffer) while the previous is drawing
	cur_buf = (cur_buf + 1) & 1;
	glBindBuffer(GL_ARRAY_BUFFER, vbo[cur_buf]);
	pvarr = (PVertex*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
#endif
	pvptr = pvarr;
}

static void pdraw_start(const psys_emitter *em, void *cls)
{
	ParticleSystem *ps = (ParticleSystem*)cls;

	init_particle_draw();

	num_buffered = 0;

#ifdef USE_IBO
	// bind the particle index buffer which is static
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
#endif

#ifdef USE_VBO
	// map the current write buffer
	glBindBuffer(GL_ARRAY_BUFFER, vbo[cur_buf]);
	pvarr = (PVertex*)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
#endif
	pvptr = pvarr;

	Texture *tex = ps->get_attr()->get_texture();
	if(tex) {
		tex->bind();
	} else {
		blank_tex->bind();
	}
	psdr->bind();

	glDisable(GL_DEPTH_TEST);
	glDisable(GL_CULL_FACE);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE);
	glDepthMask(0);

	glEnableVertexAttribArray(MESH_ATTR_VERTEX);
	glEnableVertexAttribArray(MESH_ATTR_COLOR);
	glEnableVertexAttribArray(MESH_ATTR_TEXCOORD);

#ifdef USE_VBO
	glVertexAttribPointer(MESH_ATTR_VERTEX, 3, GL_FLOAT, GL_FALSE, sizeof *pvarr, (void*)((char*)&pvarr->pos - (char*)pvarr));
	glVertexAttribPointer(MESH_ATTR_COLOR, 4, GL_FLOAT, GL_TRUE, sizeof *pvarr, (void*)((char*)&pvarr->color - (char*)pvarr));
	glVertexAttribPointer(MESH_ATTR_TEXCOORD, 2, GL_FLOAT, GL_FALSE, sizeof *pvarr, (void*)((char*)&pvarr->texcoord - (char*)pvarr));
#else
	glVertexAttribPointer(MESH_ATTR_VERTEX, 3, GL_FLOAT, GL_FALSE, sizeof *pvarr, (void*)&pvarr->pos);
	glVertexAttribPointer(MESH_ATTR_COLOR, 4, GL_FLOAT, GL_TRUE, sizeof *pvarr, (void*)&pvarr->color);
	glVertexAttribPointer(MESH_ATTR_TEXCOORD, 2, GL_FLOAT, GL_FALSE, sizeof *pvarr, (void*)&pvarr->texcoord);
#endif
}

static void pdraw(const psys_emitter *em, const psys_particle *part, void *cls)
{
	ParticleSystem *ps = (ParticleSystem*)cls;

	static const Vector3 pv[] = {
		Vector3(-0.5, -0.5, 0),
		Vector3(0.5, -0.5, 0),
		Vector3(0.5, 0.5, 0),
		Vector3(-0.5, 0.5, 0)
	};
	static const Vector2 tex[] = {
		Vector2(0, 0), Vector2(0, 1), Vector2(1, 1), Vector2(1, 0)
	};
	Vector4 color = Vector4(part->color.x, part->color.y, part->color.z, part->alpha);

	for(int i=0; i<4; i++) {
		pvptr->color = color;
		pvptr->pos = pv[i] * part->size + part->pos;
		pvptr->texcoord = tex[i];
		pvptr++;
	}
	// XXX we don't need billboarding for this game, so don't bother

	// if we reached the maximum number of buffered particles, draw them
	if(++num_buffered >= MAX_DRAW_PART) {
		pdraw_flush();	// this will reset the counter
	}
}

static void pdraw_end(const psys_emitter *em, void *cls)
{
	// if we have leftover particles buffered, draw them before returning
	if(num_buffered) {
		pdraw_flush();
	}

	// cleanup
	glDisableVertexAttribArray(MESH_ATTR_VERTEX);
	glDisableVertexAttribArray(MESH_ATTR_COLOR);
	glDisableVertexAttribArray(MESH_ATTR_TEXCOORD);

#ifdef USE_VBO
	glUnmapBuffer(GL_ARRAY_BUFFER);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
#endif
#ifdef USE_IBO
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
#endif

	glDepthMask(1);
	glDisable(GL_BLEND);

	glEnable(GL_DEPTH_TEST);
	glEnable(GL_CULL_FACE);
}

static unsigned int psys_load_texture(const char *fname, void *cls)
{
	ParticleSystemAttributes *attr = (ParticleSystemAttributes*)cls;

	Texture *tex = texset.get(fname);
	if(tex) {
		attr->set_texture(tex);
		return tex->get_id();
	}
	return 0;
}
